package com.bluebench.bigul.service.processor;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.Stack;

import com.bluebench.bigul.domain.Element;
import com.bluebench.bigul.domain.Element.Attribute;
import com.bluebench.bigul.domain.Element.Parameter;
import com.bluebench.bigul.domain.Tag;
import com.bluebench.bigul.domain.Types;
import com.bluebench.bigul.exception.ChainedAttributeException;
import com.bluebench.bigul.exception.GraphicInitializationException;
import com.bluebench.bigul.service.TypeFactory;
import com.bluebench.bigul.utility.reflection.ReflectionUtil;

public class AttributeProcessor extends GenericProcessor{
	
	private Object ob;	//object to which attribute will be applied
	private Tag tag; //attribute will be set here
	private Object returnOb;
	private Stack returnObjStack;
	
	public AttributeProcessor() {
		returnObjStack = new Stack();
	}
	
	void set(Object o){
		ob = o;
	}
	
	@Override
	public Object process() throws SecurityException, IllegalArgumentException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchFieldException, GraphicInitializationException {
		
		Attribute attribute = (Attribute)tag;
		List<Parameter> parameters = attribute.getParameters();
		try{
		setAttribute(ob, attribute, parameters);
		if(attribute.isChained()){
			for(Element.Attribute a : attribute.getAttributes()){
				ob = returnObjStack.peek();
				tag = a;
				process();
				returnObjStack.pop();
			}
		}
		}catch(ChainedAttributeException e){
			returnObjStack.pop();
			if(!returnObjStack.isEmpty()){
				ob = returnObjStack.peek();	
				process();
				returnObjStack.pop();
			}
		}
		return ob;
	}
	
	@Override
	public void set(Tag t) {
		tag = t;		
	}
	
	/**
	 * 
	 * @param ob
	 * @param attribute
	 * @param parameters
	 * @return element object after setting attribute values
	 * @throws SecurityException
	 * @throws ClassNotFoundException
	 * @throws NoSuchMethodException
	 * @throws IllegalArgumentException
	 * @throws IllegalAccessException
	 * @throws InvocationTargetException
	 * @throws InstantiationException
	 * @throws NoSuchFieldException
	 * @throws ChainedAttributeException 
	 * @throws GraphicInitializationException 
	 */
	private Object setAttribute(Object ob, Attribute attribute, List<Parameter> parameters) throws SecurityException, ClassNotFoundException, NoSuchMethodException, IllegalArgumentException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchFieldException, ChainedAttributeException, GraphicInitializationException{
		
		Method method = getMethod(ob.getClass(),attribute,parameters);	
		if(method==null && attribute.isChained()){
			throw new ChainedAttributeException();
		}
		ArrayList values = new ArrayList();
		ArrayList collectionValues = new ArrayList();
		
		/*
		 * TODO : Potential Problem
		 * ------------------------
		 * This way of handling collection type has a potential problem that will cause application to break in following cases
		 * a. In case there are two parameters One Collection type and the Other non-collection type
		 * b. In case there are multiple Collection type arguments to the method
		 */
		
		for(int i=0;i<parameters.size();i++){
			
			Parameter p = parameters.get(i);
			String type = p.getType();
			p.setProcessor(GenericProcessor.getInstance(PARAMETER_PROCESSOR));
			Object iTextParamObj = p.process();
			
			if(type!=null && type.equalsIgnoreCase("collection")){
				if(iTextParamObj instanceof ArrayList){
					collectionValues.addAll((ArrayList)iTextParamObj);
				}
			}else{
				values.add(iTextParamObj);
			}
			
		}
		
		//showParameterTypes(collectionValues);	
		//showParameterTypes(values);
				
		if(!collectionValues.isEmpty()){
			for(Object value:collectionValues){				
				method.invoke(ob,value);
			}
			
		}		
		
		if(!values.isEmpty()){
			returnOb = method.invoke(ob, values.toArray());
			returnObjStack.push(returnOb);
		}else{
			returnOb = method.invoke(ob);
			returnObjStack.push(returnOb);
		}
		
		
		
		return ob;		
	}
	
	
	private void showParameterTypes(List values){
		if(!values.isEmpty()){
			System.out.println(ob+"\n-------------------");
			for(Object o : values){
				System.out.println(o.getClass());
			}
			System.out.println("\n\n");
		}
	}
	
	
	
	private Method getMethod(Class<?> c, Attribute attribute, List<Parameter> parameterList) throws ClassNotFoundException, SecurityException, IllegalArgumentException, IllegalAccessException, InvocationTargetException {
		
		String methodName = getMethodName(attribute.getName());
		Class<?>[] params = getParameterTypes(parameterList);	
		
		Method m = ReflectionUtil.find(c,methodName,params);
		
		/* Commented for testing new method
		 
		 Method m=null;
		try {
			
			m = c.getDeclaredMethod(methodName, params);
			
		} catch (NoSuchMethodException e) {
			
			Class<?>[] polyParams = getPolymorphicParameterTypes(c, methodName, params);
			
			try {
				//Issue here
				m = c.getDeclaredMethod(methodName, polyParams);
				
			} catch (NoSuchMethodException e1) {				
				
				Class<?> cSup = c.getSuperclass();
				
				try {
					//again this may require a polymorphic param check
					m=cSup.getMethod(methodName, params);
				
				} catch (NoSuchMethodException e2) {
					
					try {
						
						polyParams = getPolymorphicParameterTypes(cSup, methodName, params);
						
						m=cSup.getMethod(methodName, polyParams);
					
					} catch (NoSuchMethodException e3) {
						// TODO Auto-generated catch block
						e3.printStackTrace();
					}
					
				}
				
			}
		}
	*/		
		return m;
		
	}
	
	/**
	 * The Method name may not always be generated as a concatination of set keyword and the attribute.
	 * Therefore we are required to have a getMethodName() method in the Generic Processor class that searches the name
	 * from a methodMap. We need to pass the attribute to the methodMap which returns the method name, If null is returned then
	 * a setter is used.     		 
	 * @param attributeName
	 * @return
	 */
	public String getMethodName(String attributeName){
		
		StringBuilder methodName = new StringBuilder(attributeName);
		methodName.setCharAt(0, Character.toUpperCase(methodName.charAt(0)));		
		
		if(methodMap.containsKey(attributeName)){
			methodName = new StringBuilder(methodMap.get(attributeName));
		}else{
			methodName = new StringBuilder("set").append(methodName);
		}
		
		return methodName.toString();
	}
	
	private Class<?>[] getParameterTypes(List<Parameter> parameters) throws ClassNotFoundException{
	
		/*
		 * TODO : Requires refactoring for different scenarios of types like
		 * a. various element, attribute, parameter types
		 * b. various default types
		 */
		Class<?>[] paramclassArray = new Class<?>[parameters.size()]; 
		
		for(int i=0;i<parameters.size();i++){
			Parameter p = parameters.get(i);
			String type = p.getType();
			Types t=null;
			
			if(type!=null){
				
				if(type.startsWith("_")){
				
					paramclassArray[i] = TypeFactory.getPrimitiveType(type.replace("_", "").trim());					
				
				}else if(type.startsWith("collection")){
					
					List<Parameter> paramCollection = (ArrayList<Parameter>)p.getValue();
					//Considering that entire collection is a homogeneous collection.
					Parameter param = paramCollection.get(0);
					String paramType = param.getType();
					t = Types.getType(paramType);
					paramclassArray[i] = Class.forName(t.getClassName());
		
					
				}else if(type.startsWith("arraylist")){
					paramclassArray[i] = Class.forName("java.util.ArrayList");
				}else{
			
					t = Types.getType(type);
					paramclassArray[i]=Class.forName(t.getClassName());
				
				}
				
			}else{
				
				Object value = parameters.get(i).getValue();
				if(value instanceof Element){
					Element paramVal = (Element)value;
					t = Types.getType(paramVal.getType());
				}else{
					t = Types.DEFAULT;
				}					
				paramclassArray[i]=Class.forName(t.getClassName());
			}
			
		}
		
		return paramclassArray;
		
	}
	
	private Class<?>[] getPolymorphicParameterTypes(Class<?> c, String methodName, Class<?>[] argTypes){
		
		//TODO : Follow this - http://stackoverflow.com/questions/3083786/using-java-lang-reflect-getmethod-with-polymorphic-methods
		Method[] methods = c.getDeclaredMethods();
		Class<?>[] retParamTypes = new Class<?>[argTypes.length];
		for(Method m : methods){
			if(m.getName().equals(methodName)){
				Class<?>[] paramTypes = m.getParameterTypes();
				if(paramTypes.length==argTypes.length){
					for(int i=0;i<paramTypes.length;i++){
						if(argTypes[i].equals(paramTypes[i])||paramTypes[i].isAssignableFrom(argTypes[i])){
							
							retParamTypes[i]=paramTypes[i];
						}
					}
				}				
			}
		}
		return retParamTypes;
	}
	
	String getMethodPrefix(String attribute){
		
		return null;
	}

	
}
